// Copyright (C) 1998-2001 by Jason Hunter <jhunter_AT_acm_DOT_org>.
// All rights reserved.  Use of this class is limited.
// Please see the LICENSE for more information.

package com.oreilly.servlet;

import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

import com.oreilly.servlet.multipart.*;

/**
 * A utility class to handle <code>multipart/form-data</code> requests,
 * the kind of requests that support file uploads.  This class emulates the
 * interface of <code>HttpServletRequest</code>, making it familiar to use.
 * It uses a "push" model where any incoming files are read and saved directly
 * to disk in the constructor. If you wish to have more flexibility, e.g.
 * write the files to a database, use the "pull" model
 * <code>MultipartParser</code> instead.
 * <p>
 * This class can receive arbitrarily large files (up to an artificial limit
 * you can set), and fairly efficiently too.
 * It cannot handle nested data (multipart content within multipart content).
 * It <b>can</b> now with the latest release handle internationalized content
 * (such as non Latin-1 filenames).
 * <p>
 * To avoid collisions and have fine control over file placement, there's a
 * constructor variety that takes a pluggable FileRenamePolicy implementation.
 * A particular policy can choose to rename or change the location of the file
 * before it's written.
 * <p>
 * See the included upload.war for an example of how to use this class.
 * <p>
 * The full file upload specification is contained in experimental RFC 1867,
 * available at <a href="http://www.ietf.org/rfc/rfc1867.txt">
 * http://www.ietf.org/rfc/rfc1867.txt</a>.
 *
 * @author Jason Hunter
 * @author Geoff Soutter
 * @version 1.0, 1998/09/18<br>
 * @see MultipartParser
 */
public class MultipartRequest {

    private static final int DEFAULT_MAX_POST_SIZE = 1024 * 1024;  // 1 Meg

    protected Hashtable parameters = new Hashtable();  // name - Vector of values
    protected Hashtable files = new Hashtable();       // name - UploadedFile

    /**
     * Constructs a new MultipartRequest to handle the specified request,
     * saving any uploaded files to the given directory, and limiting the
     * upload size to 1 Megabyte.  If the content is too large, an
     * IOException is thrown.  This constructor actually parses the
     * <tt>multipart/form-data</tt> and throws an IOException if there's any
     * problem reading or parsing the request.
     *
     * @param request       the servlet request.
     * @param saveDirectory the directory in which to save any uploaded files.
     * @throws IOException if the uploaded content is larger than 1 Megabyte
     *                     or there's a problem reading or parsing the request.
     */
    public MultipartRequest(HttpServletRequest request,
                            String saveDirectory) throws IOException {
        this(request, saveDirectory, DEFAULT_MAX_POST_SIZE);
    }

    /**
     * Constructs a new MultipartRequest to handle the specified request,
     * saving any uploaded files to the given directory, and limiting the
     * upload size to the specified length.  If the content is too large, an
     * IOException is thrown.  This constructor actually parses the
     * <tt>multipart/form-data</tt> and throws an IOException if there's any
     * problem reading or parsing the request.
     *
     * @param request       the servlet request.
     * @param saveDirectory the directory in which to save any uploaded files.
     * @param maxPostSize   the maximum size of the POST content.
     * @throws IOException if the uploaded content is larger than
     *                     <tt>maxPostSize</tt> or there's a problem reading or parsing the request.
     */
    public MultipartRequest(HttpServletRequest request,
                            String saveDirectory,
                            int maxPostSize) throws IOException {
        this(request, saveDirectory, maxPostSize, null, null);
    }

    /**
     * Constructs a new MultipartRequest to handle the specified request,
     * saving any uploaded files to the given directory, and limiting the
     * upload size to the specified length.  If the content is too large, an
     * IOException is thrown.  This constructor actually parses the
     * <tt>multipart/form-data</tt> and throws an IOException if there's any
     * problem reading or parsing the request.
     *
     * @param request       the servlet request.
     * @param saveDirectory the directory in which to save any uploaded files.
     * @param encoding      the encoding of the response, such as ISO-8859-1
     * @throws IOException if the uploaded content is larger than
     *                     1 Megabyte or there's a problem reading or parsing the request.
     */
    public MultipartRequest(HttpServletRequest request,
                            String saveDirectory,
                            String encoding) throws IOException {
        this(request, saveDirectory, DEFAULT_MAX_POST_SIZE, encoding, null);
    }

    /**
     * Constructs a new MultipartRequest to handle the specified request,
     * saving any uploaded files to the given directory, and limiting the
     * upload size to the specified length.  If the content is too large, an
     * IOException is thrown.  This constructor actually parses the
     * <tt>multipart/form-data</tt> and throws an IOException if there's any
     * problem reading or parsing the request.
     *
     * @param request       the servlet request.
     * @param saveDirectory the directory in which to save any uploaded files.
     * @param maxPostSize   the maximum size of the POST content.
     * @param policy        the rules for renaming in case of file name collisions
     * @throws IOException if the uploaded content is larger than
     *                     <tt>maxPostSize</tt> or there's a problem reading or parsing the request.
     */
    public MultipartRequest(HttpServletRequest request,
                            String saveDirectory,
                            int maxPostSize,
                            FileRenamePolicy policy) throws IOException {
        this(request, saveDirectory, maxPostSize, null, policy);
    }

    /**
     * Constructs a new MultipartRequest to handle the specified request,
     * saving any uploaded files to the given directory, and limiting the
     * upload size to the specified length.  If the content is too large, an
     * IOException is thrown.  This constructor actually parses the
     * <tt>multipart/form-data</tt> and throws an IOException if there's any
     * problem reading or parsing the request.
     *
     * @param request       the servlet request.
     * @param saveDirectory the directory in which to save any uploaded files.
     * @param maxPostSize   the maximum size of the POST content.
     * @param encoding      the encoding of the response, such as ISO-8859-1
     * @throws IOException if the uploaded content is larger than
     *                     <tt>maxPostSize</tt> or there's a problem reading or parsing the request.
     */
    public MultipartRequest(HttpServletRequest request,
                            String saveDirectory,
                            int maxPostSize,
                            String encoding) throws IOException {
        this(request, saveDirectory, maxPostSize, encoding, null);
    }

    /**
     * Constructs a new MultipartRequest to handle the specified request,
     * saving any uploaded files to the given directory, and limiting the
     * upload size to the specified length.  If the content is too large, an
     * IOException is thrown.  This constructor actually parses the
     * <tt>multipart/form-data</tt> and throws an IOException if there's any
     * problem reading or parsing the request.
     * <p>
     * To avoid file collisions, this constructor takes an implementation of the
     * FileRenamePolicy interface to allow a pluggable rename policy.
     *
     * @param request       the servlet request.
     * @param saveDirectory the directory in which to save any uploaded files.
     * @param maxPostSize   the maximum size of the POST content.
     * @param encoding      the encoding of the response, such as ISO-8859-1
     * @param policy        a pluggable file rename policy
     * @throws IOException if the uploaded content is larger than
     *                     <tt>maxPostSize</tt> or there's a problem reading or parsing the request.
     */
    public MultipartRequest(HttpServletRequest request,
                            String saveDirectory,
                            int maxPostSize,
                            String encoding,
                            FileRenamePolicy policy) throws IOException {
        // Sanity check values
        if (request == null)
            throw new IllegalArgumentException("request cannot be null");
        if (saveDirectory == null)
            throw new IllegalArgumentException("saveDirectory cannot be null");
        if (maxPostSize <= 0) {
            throw new IllegalArgumentException("maxPostSize must be positive");
        }

        // Save the dir
        File dir = new File(saveDirectory);

        // Check saveDirectory is truly a directory
        if (!dir.isDirectory())
            throw new IllegalArgumentException("Not a directory: " + saveDirectory);

        // Check saveDirectory is writable
        if (!dir.canWrite())
            throw new IllegalArgumentException("Not writable: " + saveDirectory);

        // Parse the incoming multipart, storing files in the dir provided,
        // and populate the meta objects which describe what we found
        MultipartParser parser =
                new MultipartParser(request, maxPostSize, true, true, encoding);

        // Some people like to fetch query string parameters from
        // MultipartRequest, so here we make that possible.  Thanks to
        // Ben Johnson, ben.johnson@merrillcorp.com, for the idea.
        if (request.getQueryString() != null) {
            // Let HttpUtils create a name->String[] structure
            Hashtable queryParameters =
                    HttpUtils.parseQueryString(request.getQueryString());
            // For our own use, name it a name->Vector structure
            Enumeration queryParameterNames = queryParameters.keys();
            while (queryParameterNames.hasMoreElements()) {
                Object paramName = queryParameterNames.nextElement();
                String[] values = (String[]) queryParameters.get(paramName);
                Vector newValues = new Vector();
                for (int i = 0; i < values.length; i++) {
                    newValues.add(values[i]);
                }
                parameters.put(paramName, newValues);
            }
        }

        Part part;
        while ((part = parser.readNextPart()) != null) {
            String name = part.getName();
            if (name == null) {
                throw new IOException(
                        "Malformed input: parameter name missing (known Opera 7 bug)");
            }
            if (part.isParam()) {
                // It's a parameter part, add it to the vector of values
                ParamPart paramPart = (ParamPart) part;
                String value = paramPart.getStringValue();
                Vector existingValues = (Vector) parameters.get(name);
                if (existingValues == null) {
                    existingValues = new Vector();
                    parameters.put(name, existingValues);
                }
                existingValues.addElement(value);
            } else if (part.isFile()) {
                // It's a file part
                FilePart filePart = (FilePart) part;
                String fileName = filePart.getFileName();
                if (fileName != null) {
                    filePart.setRenamePolicy(policy);  // null policy is OK
                    // The part actually contained a file
                    filePart.writeTo(dir);
                    files.put(name, new UploadedFile(dir.toString(),
                            filePart.getFileName(),
                            fileName,
                            filePart.getContentType()));
                } else {
                    // The field did not contain a file
                    files.put(name, new UploadedFile(null, null, null, null));
                }
            }
        }
    }


    /**
     * 新增接口
     * 返回 InputStream
     *
     * @Author Ani_aierJun
     */

    public MultipartRequest(HttpServletRequest request,
                            int maxPostSize,
                            String encoding,
                            InputStreamCallBack inputStreamCallBack) throws IOException {
        // Sanity check values
        if (request == null)
            throw new IllegalArgumentException("request cannot be null");
        if (inputStreamCallBack == null)
            throw new IllegalArgumentException("InputStreamCallBack cannot be null");
        if (maxPostSize <= 0) {
            throw new IllegalArgumentException("maxPostSize must be positive");
        }

        // Save the dir
//    File dir = new File(saveDirectory);

        // Check saveDirectory is truly a directory
//    if (!dir.isDirectory())
//      throw new IllegalArgumentException("Not a directory: " + saveDirectory);

        // Check saveDirectory is writable
//    if (!dir.canWrite())
//      throw new IllegalArgumentException("Not writable: " + saveDirectory);

        // Parse the incoming multipart, storing files in the dir provided,
        // and populate the meta objects which describe what we found
        MultipartParser parser =
                new MultipartParser(request, maxPostSize, true, true, encoding);

        // Some people like to fetch query string parameters from
        // MultipartRequest, so here we make that possible.  Thanks to
        // Ben Johnson, ben.johnson@merrillcorp.com, for the idea.
        if (request.getQueryString() != null) {
            // Let HttpUtils create a name->String[] structure
            Hashtable queryParameters =
                    HttpUtils.parseQueryString(request.getQueryString());
            // For our own use, name it a name->Vector structure
            Enumeration queryParameterNames = queryParameters.keys();
            while (queryParameterNames.hasMoreElements()) {
                Object paramName = queryParameterNames.nextElement();
                String[] values = (String[]) queryParameters.get(paramName);
                Vector newValues = new Vector();
                for (int i = 0; i < values.length; i++) {
                    newValues.add(values[i]);
                }
                parameters.put(paramName, newValues);
            }
        }

        Part part;
        while ((part = parser.readNextPart()) != null) {
            String name = part.getName();
            if (name == null) {
                throw new IOException(
                        "Malformed input: parameter name missing (known Opera 7 bug)");
            }
            if (part.isParam()) {
                // It's a parameter part, add it to the vector of values
                ParamPart paramPart = (ParamPart) part;
                String value = paramPart.getStringValue();
                Vector existingValues = (Vector) parameters.get(name);
                if (existingValues == null) {
                    existingValues = new Vector();
                    parameters.put(name, existingValues);
                }
                existingValues.addElement(value);
            } else if (part.isFile()) {
                // It's a file part
                FilePart filePart = (FilePart) part;
                String fileName = filePart.getFileName();
                if (fileName != null) {
                    inputStreamCallBack.callBack(filePart.getInputStream());
                    files.put(name, new UploadedFile(null,
                            filePart.getFileName(),
                            fileName,
                            filePart.getContentType()));
                } else {
                    // The field did not contain a file
                    files.put(name, new UploadedFile(null, null, null, null));
                }
            }
        }
    }

    /**
     * Constructor with an old signature, kept for backward compatibility.
     * Without this constructor, a servlet compiled against a previous version
     * of this class (pre 1.4) would have to be recompiled to link with this
     * version.  This constructor supports the linking via the old signature.
     * Callers must simply be careful to pass in an HttpServletRequest.
     */
    public MultipartRequest(ServletRequest request,
                            String saveDirectory) throws IOException {
        this((HttpServletRequest) request, saveDirectory);
    }

    /**
     * Constructor with an old signature, kept for backward compatibility.
     * Without this constructor, a servlet compiled against a previous version
     * of this class (pre 1.4) would have to be recompiled to link with this
     * version.  This constructor supports the linking via the old signature.
     * Callers must simply be careful to pass in an HttpServletRequest.
     */
    public MultipartRequest(ServletRequest request,
                            String saveDirectory,
                            int maxPostSize) throws IOException {
        this((HttpServletRequest) request, saveDirectory, maxPostSize);
    }

    /**
     * Returns the names of all the parameters as an Enumeration of
     * Strings.  It returns an empty Enumeration if there are no parameters.
     *
     * @return the names of all the parameters as an Enumeration of Strings.
     */
    public Enumeration getParameterNames() {
        return parameters.keys();
    }

    /**
     * Returns the names of all the uploaded files as an Enumeration of
     * Strings.  It returns an empty Enumeration if there are no file input
     * fields on the form.  Any file input field that's left empty will result
     * in a FilePart with null contents.  Each file name is the name specified
     * by the form, not by the user.
     *
     * @return the names of all the uploaded files as an Enumeration of Strings.
     */
    public Enumeration getFileNames() {
        return files.keys();
    }

    /**
     * Returns the value of the named parameter as a String, or null if
     * the parameter was not sent or was sent without a value.  The value
     * is guaranteed to be in its normal, decoded form.  If the parameter
     * has multiple values, only the last one is returned (for backward
     * compatibility).  For parameters with multiple values, it's possible
     * the last "value" may be null.
     *
     * @param name the parameter name.
     * @return the parameter value.
     */
    public String getParameter(String name) {
        try {
            Vector values = (Vector) parameters.get(name);
            if (values == null || values.size() == 0) {
                return null;
            }
            String value = (String) values.elementAt(values.size() - 1);
            return value;
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * Returns the values of the named parameter as a String array, or null if
     * the parameter was not sent.  The array has one entry for each parameter
     * field sent.  If any field was sent without a value that entry is stored
     * in the array as a null.  The values are guaranteed to be in their
     * normal, decoded form.  A single value is returned as a one-element array.
     *
     * @param name the parameter name.
     * @return the parameter values.
     */
    public String[] getParameterValues(String name) {
        try {
            Vector values = (Vector) parameters.get(name);
            if (values == null || values.size() == 0) {
                return null;
            }
            String[] valuesArray = new String[values.size()];
            values.copyInto(valuesArray);
            return valuesArray;
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * Returns the filesystem name of the specified file, or null if the
     * file was not included in the upload.  A filesystem name is the name
     * specified by the user.  It is also the name under which the file is
     * actually saved.
     *
     * @param name the html page's file parameter name.
     * @return the filesystem name of the file.
     */
    public String getFilesystemName(String name) {
        try {
            UploadedFile file = (UploadedFile) files.get(name);
            return file.getFilesystemName();  // may be null
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * Returns the original filesystem name of the specified file (before any
     * renaming policy was applied), or null if the file was not included in
     * the upload.  A filesystem name is the name specified by the user.
     *
     * @param name the html page's file parameter name.
     * @return the original file name of the file.
     */
    public String getOriginalFileName(String name) {
        try {
            UploadedFile file = (UploadedFile) files.get(name);
            return file.getOriginalFileName();  // may be null
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * Returns the content type of the specified file (as supplied by the
     * client browser), or null if the file was not included in the upload.
     *
     * @param name the html page's file parameter name.
     * @return the content type of the file.
     */
    public String getContentType(String name) {
        try {
            UploadedFile file = (UploadedFile) files.get(name);
            return file.getContentType();  // may be null
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * Returns a File object for the specified file saved on the server's
     * filesystem, or null if the file was not included in the upload.
     *
     * @param name the html page's file parameter name.
     * @return a File object for the named file.
     */
    public File getFile(String name) {
        try {
            UploadedFile file = (UploadedFile) files.get(name);
            return file.getFile();  // may be null
        } catch (Exception e) {
            return null;
        }
    }
}


// A class to hold information about an uploaded file.
//
class UploadedFile {

    private String dir;
    private String filename;
    private String original;
    private String type;

    UploadedFile(String dir, String filename, String original, String type) {
        this.dir = dir;
        this.filename = filename;
        this.original = original;
        this.type = type;
    }

    public String getContentType() {
        return type;
    }

    public String getFilesystemName() {
        return filename;
    }

    public String getOriginalFileName() {
        return original;
    }

    public File getFile() {
        if (dir == null || filename == null) {
            return null;
        } else {
            return new File(dir + File.separator + filename);
        }
    }
}

